Skip to content

feat(sdk): add webhook helper for forward-route handlers#419

Merged
danielmillerp merged 3 commits into
nextfrom
dm/sdk-webhook-helper
Jun 23, 2026
Merged

feat(sdk): add webhook helper for forward-route handlers#419
danielmillerp merged 3 commits into
nextfrom
dm/sdk-webhook-helper

Conversation

@danielmillerp

@danielmillerp danielmillerp commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

What

Adds agentex.lib.sdk.utils.webhooks.handle_webhook — a reusable helper an agent calls from a forward-route handler (a @acp.post(...) route that the server's /agents/forward/name/{agent}/{path} ingress proxies to).

It does, agent-side via adk:

  • shape the payload (generic, or GitHub/Gitea PR → clean title+body+diff prompt),
  • resolve params — inline, or fetched from a params_source URL (config-by-id; the platform's config-resolve endpoint),
  • session continuity — get-or-create a task on a stable session key so repeat events (PR opened/synchronize/…) fold into one task,
  • dispatch the turn (sync message/send returns the reply inline; async event/send, with optional wait to poll),
  • stamp canonical task_metadata (channel/peer_id/sender) plus any source-returned metadata (which can't override the canonical fields).

Why

Triggering an agent from a webhook is already supported via the forward mechanism (which has built-in GitHub/Slack signature auth). This helper provides the shaping/resolve/dispatch convenience on that supported path instead of a parallel ingress, so an agent's webhook handler is ~10 lines.

Before — every agent hand-writes the full pipeline in its handler: shape the raw payload into a prompt, fetch+map the config, hash a stable session key, get-or-create the task, dispatch, then dig the reply back out of the message list. Each agent re-implements the edge cases (diff truncation, sync vs async, id-less messages) and gets them wrong in its own way.

After — the same five steps become keyword arguments; the messy implementation lives once in webhooks.py:

@acp.post("/github-pr")
async def github_pr(request: Request):
    body = await request.json()
    result = await handle_webhook(
        agent_name="my-agent",
        payload=body,
        acp_type="sync",
        shaper="github_pr",
        params_source="https://<host>/public/v5/agent_configs/<id>/resolve",
        params_source_headers={"x-api-key": ..., "x-selected-account-id": ...},
        wait=True,
    )
    return {"task_id": result.task_id, "reply": result.reply}

Config-by-id is ingress-independent: point params_source at the platform's config-resolve endpoint (the scaleapi GET /v5/agent_configs/{id}/resolve server-side counterpart) and the resolved params are forwarded opaquely to task/create.

Design notes (open-source SDK)

This module ships in the open-source SDK, so two things were kept deliberate:

  • No secrets / internal coupling. Credentials come from the caller via params_source_headers (docstring uses ... / <host> placeholders). It shapes only public webhook formats (GitHub/Gitea/Slack) and forwards opaque params; the config-resolve endpoint is referenced generically, not by an internal URL. Self-contained leaf module (adk + stdlib).
  • Formats data, doesn't dictate behavior. shape_github_pr emits facts only (title / action / URL / description / diff) — no "review this PR" instruction is baked in. The actual task lives in the config's system_prompt, not the helper. The opinionated parts (shaper enum generic/github_pr; MAX_BODY_CHARS/MAX_DIFF_CHARS constants) are opt-in conveniences over a fully-generic base — shaper="generic" + inline params sidesteps both, so nothing traps the user. (fetch is injectable; shaper is currently a closed enum.)

Testing

  • 14 unit tests (tests/lib/test_webhooks.py): session-key folding, generic + GitHub-PR shaping, params/config-by-id resolution (envelope + bare-object + error), and handle_webhook sync/async with adk mocked. ruff clean.

🤖 Generated with Claude Code

Greptile Summary

This PR adds agentex.lib.sdk.utils.webhooks.handle_webhook, a reusable helper for forward-route handlers that consolidates the five-step pipeline (shape → resolve params → get-or-create task → dispatch → extract reply) that every webhook-driven agent was previously hand-rolling.

  • Payload shaping: render_generic and shape_github_pr convert raw webhook bodies into a structured prompt, with peer_id-based session continuity so repeated PR events fold into one task instead of spawning new ones.
  • Params resolution: supports inline params or a remote params_source URL; resolved params are forwarded opaquely to task/create, and source-returned metadata is merged onto the task (canonical fields win).
  • Async reply polling: _await_reply snapshots pre-event message ids and count so a reused task's prior reply is never returned as the new one; id-less messages are handled via positional boundary.

Confidence Score: 4/5

Safe to merge after resolving the inline-params-plus-params_source silent data loss.

The module is well-structured and regression tests cover session-continuity, metadata-merge, stale-reply, and id-less message scenarios. The one present defect is that inline params are silently overwritten when params_source is also provided, with no warning or error surfaced to the caller.

src/agentex/lib/sdk/utils/webhooks.py — handle_webhook params resolution block (lines 283-287)

Important Files Changed

Filename Overview
src/agentex/lib/sdk/utils/webhooks.py New webhook helper implementing payload shaping, params resolution, session continuity, and reply dispatch; one logic issue: inline params are silently discarded when params_source is also provided.
tests/lib/test_webhooks.py 14 unit tests covering session-key folding, generic and GitHub-PR shaping, remote params resolution, sync/async dispatch, stale-reply regression, and id-less message regression.
.stats.yml Auto-generated SDK stats file — updated OpenAPI spec URL and hash, no logic changes.

Reviews (7): Last reviewed commit: "Merge branch 'next' into dm/sdk-webhook-..." | Re-trigger Greptile

@danielmillerp danielmillerp changed the base branch from main to next June 22, 2026 04:22
@danielmillerp danielmillerp force-pushed the dm/sdk-webhook-helper branch 2 times, most recently from 73fc765 to 62aaff5 Compare June 22, 2026 04:23
Comment thread src/agentex/lib/sdk/utils/webhooks.py Outdated
Comment thread src/agentex/lib/sdk/utils/webhooks.py
Comment thread src/agentex/lib/sdk/utils/webhooks.py
@danielmillerp danielmillerp force-pushed the dm/sdk-webhook-helper branch from 62aaff5 to e91aff6 Compare June 22, 2026 04:36
Comment thread src/agentex/lib/sdk/utils/webhooks.py Outdated
Comment thread src/agentex/lib/sdk/utils/webhooks.py Outdated
Comment thread src/agentex/lib/sdk/utils/webhooks.py
Comment thread src/agentex/lib/sdk/utils/webhooks.py
Comment thread src/agentex/lib/sdk/utils/webhooks.py

@declan-scale declan-scale left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple questions, also wondering if we want to wait for the resolve pr to land and put it in the sgp sdk and then use that rather than raw fetch.

Comment thread src/agentex/lib/sdk/utils/webhooks.py Outdated
Comment thread src/agentex/lib/sdk/utils/webhooks.py Outdated
Comment thread src/agentex/lib/sdk/utils/webhooks.py
@danielmillerp danielmillerp force-pushed the dm/sdk-webhook-helper branch from 0d11673 to 66a3997 Compare June 23, 2026 00:58
@smoreinis

Copy link
Copy Markdown
Contributor

this looks good to me! i'll let Declan give the final approve since he had some other comments here too, lmk if you need a stamp

@danielmillerp danielmillerp force-pushed the dm/sdk-webhook-helper branch from 66a3997 to 80231e0 Compare June 23, 2026 19:26
Comment thread src/agentex/lib/sdk/utils/webhooks.py Outdated
Comment thread src/agentex/lib/sdk/utils/webhooks.py Outdated
@danielmillerp

Copy link
Copy Markdown
Contributor Author

Rebased this branch onto origin/main and force-pushed the updated head: 80231e0.

What changed during the rebase:

  • Dropped the compat/version-guard commit because that patch is already upstream on main.
  • Resolved the .stats.yml conflict in the codegen metadata commit by keeping the webhook-helper PR metadata.
  • Left the webhook helper code/tests intact.

Verified locally:

  • rye run ruff check --no-cache src/agentex/lib/sdk/utils/webhooks.py tests/lib/test_webhooks.py
  • rye run pytest tests/lib/test_webhooks.py -n 0 -p no:cacheprovider

Note: GitHub still shows the PR as BEHIND because the PR base is currently next, while this branch is rebased onto main as requested.

Add agentex.lib.sdk.utils.webhooks.handle_webhook — a reusable helper an agent
calls from a forward-route handler (@acp.post on the route the server's
/agents/forward/name/{agent}/{path} ingress proxies to). It shapes the payload
(generic or GitHub PR), resolves task params (inline or fetched from a config
resolve URL for config-by-id), get-or-creates a task on a stable session key so
repeat events fold into one task, drives the turn (sync message / async event),
and returns/polls the reply.

This keeps webhook triggering on the supported forward mechanism + its built-in
GitHub/Slack signature auth, instead of a parallel ingress. Config-by-id is
ingress-independent: point params_source at the platform's config-resolve endpoint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@danielmillerp danielmillerp force-pushed the dm/sdk-webhook-helper branch from 80231e0 to b03fdd0 Compare June 23, 2026 20:11
@danielmillerp danielmillerp enabled auto-merge (squash) June 23, 2026 20:18
@danielmillerp danielmillerp merged commit 514075d into next Jun 23, 2026
55 checks passed
@danielmillerp danielmillerp deleted the dm/sdk-webhook-helper branch June 23, 2026 20:25
@stainless-app stainless-app Bot mentioned this pull request Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants